mysql 锁,事务,隔离级别

概念

事务

  原子性:事务必须是一个自动工作的单元,要么全部执行,要么全部不执行.
  一致性:事务结束的时候,所有的内部数据都是正确的。
  隔离性:并发多个事务时,各个事务不干涉内部数据,处理的都是另外一个事务处理之前或之后的数据。
  持久性:事务提交之后,数据是永久性的,不可再回滚。
  在mysql 中start transaction with consistent snapshot,这个是立即开启事务,无论是否使用了表。而begin则是在操作第一个表的时候才开始开启事务。

  锁分为3个级别,分别是全局锁,表锁,行锁。
  全局锁:Flush tables with read lock 这个来来加全局锁,整个库都被锁起来了,只能读,不能写操作。一般用于全局逻辑备份。
  但是针对innodb的可重复读的级别下是可以直接开启一个事务来进行全局数据备份。这个全局锁主要针对mysql里面的非innodb引擎。
  表级锁:表锁,元数据锁。
  表锁:lock tables t1 read/write,给这个表加了锁,那就无法对这个表操纵数据。我们可以用unlock tables主动释放锁。也可以客户端断开连接的时候自动释放。read是只能读,write是无法读写。
  元数据锁:这个锁很有意思,当访问一个表(增删改查),会对这个表增加一个MDL读锁。而当对这个表结构做修改时,就会加一个MDL写锁。读锁之间不互斥,没有任何影响,但是写锁和读锁,写锁和写锁都是互斥的,必须等待前面的完成,才可以继续。
  当我们对表进行字段修改时,那么会扫描整个表的数据。因此我们需要注意,加字段对数据库的影响。
  存在这么一种情况,一个表,被多个连接事务读取数据,没有释放,接着这个表要进行表结构修改,需要获取MDL写锁,但是前面的事务不释放,那就无法获取到锁。只能阻塞,但是后面的请求都要等前面的获取到写锁,且释放锁才可以获取到读锁。那么整个表都不可用。
  对于高频,长事务表,我们可以在alter语句里面加等待时间,然后DBA手动重试。

  这里主要说2个行级锁,共享锁(s),排它锁(x).
  共享锁:事务T对数据对象a加了s锁,那么这个事务T只能对数据读而不能写,而其他事务也不能对这个数据加其他锁,只能加S锁,直到这个事务T执行完毕,这就保证了这个数据在事务T中不变。
  排它锁:事务T对数据对象加了X锁,事务T可以对数据修改读取,但是其他事务无法对数据加其他任意锁,也不能读取和修改该数据对象。
  这个s锁和x锁都是select语句用出来的。lock in share mode,就是加读锁,for update 就是加写锁。
  next-key lock:mysql加锁的基本单位,它是行锁+间隙锁。前开后闭的区间。

死锁

  2个事务,出现循环等待的情况。事务A锁了资源a1,事务B锁了资源b1,然后事务A想获取b1的资源,锁等待。事务B想获取a1资源,锁等待,死锁产生。
  2个方案:。
  1.进入等待状态,直到超时。超时参数可以通过参数innodb_lock_wait_timeout来设置。默认是50s。一般不采用这个方式。
  2.发起死锁检测,发现死锁后,主动回滚死锁链条的某一个事务,让其他事务得以执行。将参数innodb_deadlock_detect设置为on表示开启这个逻辑。我们一般采用这个方法。
  但是这个是有代价的。该逻辑如下:每当一个事务被锁住的时候,就要看看它依赖的线程有没有被别人锁住,不断循环,最后判断是否出现循环等待,也就是死锁。
  如果是所有事务都是更新同一行的操作,那么死锁检查操作就是每个新来的被堵住的线程都要判断是否是因为自己的到来而导致了死锁,如果是1000的并发,那么其操作则是100w量级的。这样会导致cpu 100%异常。
  对于热点行数据更新问题。有以下方案:
  1.确定不是死锁,把死锁检查关掉
  2.做一个数据库中间件来判断来控制请求量
  3.在代码层面吧热门行数据分割。

四大隔离级别

  Read uncommitted (读未提交):这个可能造成脏读。其效果如下,2个事务A,B。A查询数据,不提交。B修改数据,不提交。A查询数据,发现B未提交的数据已经查询出来了。
  Read committed (读已提交):这个可以避免脏读,只能读取到提交后的数据。但是可能造成不可重复读问题。2个事务A,B。A查询数据,不提交。B修改数据,提交。A查询数据,发现B未提交的数据已经查询出来了,造成了2次查询效果不一致问题。
  Repeatable read (可重复读):这个可以避免可重复读,但是无法避免幻读。2个事务A,B。A查询数据,不提交。B在A的查询范围内insert一条数据,提交。A查询数据,发现多了几条数据。造成了幻读。mysql在rr级别下是没有幻读的
  Serializable (串行化):可以避免脏读,不可重复读,幻读的问题,但是新能较低。

mysql的Innodb的上述情况。

预备知识

2PL:Two-Phase Locking

  这个是2阶段锁机制。说的是锁操作分为两个阶段:加锁阶段与解锁阶段,并且保证加锁阶段与解锁阶段不相交。也就是说,一个事务里面,对于要加锁的数据加了锁,直到事务结束(正常commit或者rollback),才会释放锁。因此,我们建议把需要加锁的sql放在后面执行,如此加锁时间会短很多。

mvcc和innodb的mvcc

  mvcc:多版本并发控制,适用于读多于写的情况,一行数据多个版本,读取数据可能是某个历史版本。可以有效见面加锁。
  Innodb的mvcc实现,网上有多个版本解释了innodb的mvcc实现,但是很多都无法自圆其说,这里找到一个版本是专业的DBA写的(我比较信服的一个版本)这里简述下:
  1.在Mysql中MVCC是在Innodb存储引擎中得到支持的,Innodb为每行记录都实现了三个隐藏字段:
  6字节的事务ID(DB_TRX_ID)
  7字节的回滚指针(DB_ROLL_PTR)
  隐藏的ID。
  MVCC 在mysql 中的实现依赖的是 undo log 与 read view
  undo log是为回滚而用,具体内容就是copy事务前的数据库内容(行)到undo buffer,在适合的时间把undo buffer中的内容刷新到磁盘。
  read view: 主要用来判断当前版本数据的可见性。在innodb中,创建一个新事务的时候,innodb会将当前系统中的活跃事务列表(trx_sys->trx_list)创建一个副本(read view),副本中保存的是系统当前不应该被本事务看到的其他事务id列表。当用户在这个事务中要读取该行记录的时候,innodb会将该行当前的版本号与该read view进行比较。
  系统维护一个事务id,每次开启新事物都会+1。当修改数据时,才会修改对应数据行的事务id,并且指向以前的版本。
  RR特性:只有在当前事务开启前提交的数据才可见哦。
  RC特性:只要提交的数据都可见。
  1.设该行的当前事务id为trx_id_0,read view中最早的事务id为trx_id_1, 最迟的事务id为trx_id_2。
  2.如果trx_id_0< trx_id_1或者trx_id_0=当前事务id的话,那么表明该行记录所在的事务已经在所有新事务创建之前就提交了,那么无论是RR还是RC都是绝对可见的,所以该行记录的当前值是可见的。跳到步骤6.
  3.如果trx_id_0>trx_id_2的话,那么表明该行记录所在的事务在本次新事务创建之后才开启,那么显然该行记录的当前值不可见.跳到步骤5。
  4.如果trx_id_1<=trx_id_0<=trx_id_2, 那么从trx_id_1到trx_id_2进行遍历,如果trx_id_0等于他们之中的某个事务id的话,说明这个记录行修改时的事务在本事务开启前没有提交,那么这行数据不可见。否则就是在本事务开启前就已经提交了,那么可见了跳到步骤5.
  5.从该行记录的DB_ROLL_PTR指针所指向的回滚段中取出最新的undo-log的版本号,将它赋值该trx_id_0,然后跳到步骤2.
  6.将该可见行的值返回。
  总结:开启一个新事务,获取到一个read view,当前事务是最大的,因为开启一个加1一下,然后查询数据,只要查询的数据的行事务id等于当前事务号,或者<当前事务号,且不在read view中,那么说明这个行事务id是在本事务开启前就提交了,所以这行是可见的,如果在read view里面或者>当前事务号,那么都是在本事务开启前没有提交的,那么对于RR来说都是不可见的。只能回滚到上一条数据中,在进行判断。
  数据可见分3个情况:等于当前事务id,下雨活跃最小事务id,不在活跃事务id表里面。  

  至于RR和RC的区别在于,RR是事务开启时获得一份ReadView,而RC则是每次语句都获取一次ReadView,这个造成了不同隔离级别的快照读的可见性的区别了。
  RC总结如下:
  开启一个新事务,不获取到一个read view,而是在查询时获取到这个Read View,只要查出的这个数据的事务id不在这个read view里面,那么就是可见的了。因为在这个read view里面,对于当前查询来说都是为提交的啊,因为是活跃的事务,而不在这个里面的都是已经提交的事务,对于RC来说,只要是提交的事务,都是可见的。
  只要你在我之前提交了,那么我的read view里面就没有你,那么我就可见你了。RC的判断很简单,每次查询时都会取一个read view,只要这个行数据不在这个我得read view里面即可。
  这个就完美解释了不同隔离级别数据的可见性问题了

Innodb的四大隔离级别与标准的区别

  RU,RC.Serializable和标准一样,但是RR不一样,RR解决了幻读的问题。 怎么实现的后面说。

一条简单的语句如何加锁?加什么锁。

 CREATE TABLE `user` (

 `id` int(11) NOT NULL,

  `city` varchar(16) NOT NULL,

  `name` varchar(16) NOT NULL,

  PRIMARY KEY (`id`),

  KEY `city` (`city`),

) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

 数据为(0,0,0)(5,5,5)(10,10,10)(15,15,15)

  1.delete from user where id=5;
  按照规则1,这个先加一个(0,5]的next-key锁,再按照规则3,给唯一索引加锁时,退化为行锁。因此只加了5这个行锁。

  2.delete from user where id=7;
  按照规则1,这个先加一个(5,10]的next-key锁,再按照规则4,等值查询,给索引加锁时,向右遍历最后一个不满足条件,退化为间隙锁,因此加锁为(5,10)。  

3.delete from user where city=5;  
按照规则1,先加锁(0,5],由于是非唯一索引,继续向右,加锁(5,10],根据规则4,退化为(5,10),因此加锁范围为(0,10)

4.delete from user where id>=5 and id<6; 
  按照规则,首先是加锁(0,5],根据规则3,给唯一索引加锁时,退化为行锁,因此变成只对5加了行锁。范围查询,继续向右,再次加锁(5,10],因此加锁范围[5,10].

  5.delete from user where city>=5 and city<6; 
  按照规则,加锁(0,5],范围查询继续向右再加锁(5,10]。因此加锁范围(0,10].

  6.delete from user where id>=5 and id<=10;
  按照规则,首先是加锁(0,5],根据规则3,给唯一索引加锁时,退化为行锁,因此变成只对5加了行锁。范围查询,继续向右,再次加锁(5,10],再次根据规则5,加锁(10,15]因此加锁范围[5,15].

  7.delete from user where id>=5 and id<6 limit 1; 
  这个和4几乎一样,但是多了一个limit,逻辑和前面一样,但是已经找到5这个数据删除了,后面的就不加锁了,因为任务完成了。

加锁规则如下:

  1.加锁的基本单位是next-key lock,

  2.只有访问到的对象才会加锁

  3.索引上的等值查询,给唯一索引加锁时,会退化为行锁。

  4.索引上的等值查询,向右遍历时且最后一个值不满足条件,next-lock退化为间隙锁。

  5.唯一索引上的范围查询会查询到第一个不满足条件的为止。

6.间隙锁之间互不冲突,只和在这个区间插入数据冲突。

参考书籍:mysql实战45讲。以上五个规则来自该作者的整理。经测试,实用有效。